package fx.mine.active.util;

import javafx.collections.ObservableList;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;

public class ObjectTableView<T> extends TableView<T> {
    /**
     * Constructs an {@code ObjectTableView} suitable for displaying objects of
     * the specified model class.
     *
     * @param modelClass Class whose fields are to be used as {@code TableColumn}s
     */
    public ObjectTableView(Class<T> modelClass) {
        new ArrayList<>(Arrays.asList(modelClass.getDeclaredFields())).forEach(field -> {
            if (field.getAnnotation(HiddenField.class) != null) {
                return;
            }
            String fieldName;
            if (field.getAnnotation(ColumnName.class) != null) {
                fieldName = field.getAnnotation(ColumnName.class).name();
            } else {
                fieldName = field.getName();
            }
            TableColumn<T, ?> column = new TableColumn<>(fieldName);
            column.setCellValueFactory(new PropertyValueFactory<>(field.getName()));
            this.getColumns().add(column);
        });
    }

    /**
     * Constructs an {@code ObjectTableView} suitable for displaying objects of the
     * type passed as parameter. Sets the objects as table rows with the column names
     * defaulting to the field names of the passed class.
     *
     * @param objects {@code ObservableList} of the objects to be used in the {@code TableView}
     * @throws UnsupportedOperationException If no objects are passed.
     *                                       Use {@code new ObjectTableView(Class clazz)} instead.
     */
    public ObjectTableView(ObservableList<T> objects) throws UnsupportedOperationException {
        if (objects == null || objects.size() == 0) {
            throw new UnsupportedOperationException("The object list supplied when instantiating " +
                    "com.jfxdev.ObjectTableView must not be null or size zero. Other constructors support this.");
        }
        new ArrayList<>(Arrays.asList(objects.get(0).getClass().getDeclaredFields())).forEach(field -> {
            if (field.getAnnotation(HiddenField.class) != null) {
                return;
            }
            String fieldName;
            if (field.getAnnotation(ColumnName.class) != null) {
                fieldName = field.getAnnotation(ColumnName.class).name();
            } else {
                fieldName = field.getName();
            }
            TableColumn<T, ?> column = new TableColumn<>(fieldName);
            column.setCellValueFactory(new PropertyValueFactory<>(field.getName()));
            this.getColumns().add(column);
        });
        this.setItems(objects);
    }

    /**
     * Renames a single table column. {@code ObjectTableView().applyColumnNameMapping()}
     * should be preferred for renaming multiple {@code TableColumn}s at once,
     * as renaming a single column requires searching through all columns in order
     * to perform the rename operation.
     *
     * @param name    {@code TableColumn}'s current name
     * @param newName {@code TableColumn}'s intended name
     */
    public void renameColumn(String name, String newName) {
        for (TableColumn<T, ?> col : this.getColumns()) {
            if (col.getText().equals(name)) {
                col.setText(newName);
                break;
            }
        }
    }

    /**
     * Applies column names from a {@code HashMap} where the key signifies the current
     * column name and the value signifies the intended name. This should be preferred
     * when renaming multiple columns, as the operation is faster than
     * {@code renameColumn(String name, String name)}.
     *
     * @param map {@code HashMap<String,String>} of structure: oldName/newName
     */
    public void applyColumnNameMapping(HashMap<String, String> map) {
        if (map == null) {
            throw new UnsupportedOperationException("Unable to apply column mapping with null map");
        }
        if (map.isEmpty()) return;
        for (TableColumn<T, ?> col : this.getColumns()) {
            if (map.containsKey(col.getText())) {
                col.setText(map.get(col.getText()));
            }
        }
    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface ColumnName {
        String name() default "";
    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface HiddenField {
    }
}
